﻿using System;
using System.Collections.Generic;
using System.Linq;
using GeoAPI.CoordinateSystems;
using GeoAPI.CoordinateSystems.Transformations;
using GeoAPI.Geometries;
using GisSharpBlog.NetTopologySuite.CoordinateSystems.Transformations;
using GisSharpBlog.NetTopologySuite.Geometries;
using ProjNet.CoordinateSystems.Transformations;

namespace AzukiMap.Tools
{
    public static class TransformUtil
    {
        private static readonly IGeometryFactory GeomFactory = new GeometryFactory();
        private static readonly GeometryUtil geomUtil = GeometryUtil.Default;
        private static readonly List<ICoordinateTransformation> TransList = new List<ICoordinateTransformation>();

        /// <summary>
        /// Transform a point object by a given CoordinateTransformation.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="ct"></param>
        /// <returns></returns>
        public static IPoint Transform(IPoint source, ICoordinateTransformation ct)
        {
            if (ct == null)
            {
                return source;
            }
            IPoint p = GeometryTransform.TransformPoint
                (GeomFactory, source, ct.MathTransform);
            p.SRID = (int) ct.TargetCS.AuthorityCode;

            return p;
        }

        /// <summary>
        /// Transform a linestring object by a given CoordinateTransformation.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="ct"></param>
        /// <returns></returns>
        public static ILineString Transform(ILineString source, ICoordinateTransformation ct)
        {
            if (ct == null)
            {
                return source;
            }
            ILineString l = GeometryTransform.TransformLineString
                (GeomFactory, source, ct.MathTransform);
            l.SRID = (int) ct.TargetCS.AuthorityCode;

            return l;
        }

        /// <summary>
        /// Transform a polygon object by a given CoordinateTransformation.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="ct"></param>
        /// <returns></returns>
        public static IPolygon Transform(IPolygon source, ICoordinateTransformation ct)
        {
            if (ct == null)
            {
                return source;
            }
            IPolygon p = GeometryTransform.TransformPolygon
                (GeomFactory, source, ct.MathTransform);
            p.SRID = (int) ct.TargetCS.AuthorityCode;

            return p;
        }


        /// <summary>
        /// Transform a point object by SRID.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        public static IPoint Transform(IPoint source, int targetSrid)
        {
            if ( source.SRID == targetSrid )
            {
                return source;
            }

            if (source.SRID == 0)
            {
                throw new ArgumentException("Source SRID is not set.", "source");
            }

            ICoordinateTransformation ct = GetTransformation(source.SRID, targetSrid);

            return Transform(source, ct);
        }

        /// <summary>
        /// Transform a linestring object by SRID.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        public static ILineString Transform(ILineString source, int targetSrid)
        {
            if ( source.SRID == targetSrid )
            {
                return source;
            }

            if (source.SRID == 0)
            {
                throw new ArgumentException("Source SRID is not set.", "source");
            }

            ICoordinateTransformation ct = GetTransformation(source.SRID, targetSrid);

            return Transform(source, ct);
        }

        /// <summary>
        /// Transform a polygon object by SRID.
        /// </summary>
        /// <param name="source"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        public static IPolygon Transform(IPolygon source, int targetSrid)
        {
            if ( source.SRID == targetSrid )
            {
                return source;
            }

            if (source.SRID == 0)
            {
                throw new ArgumentException("Source SRID is not set.", "source");
            }

            ICoordinateTransformation ct = GetTransformation(source.SRID, targetSrid);

            return Transform(source, ct);
        }

        public static IGeometry Transform(IGeometry source, int targetSrid)
        {
            if ( source.SRID == targetSrid )
            {
                return source;
            }

            if (source.SRID == 0)
            {
                throw new ArgumentException("Source SRID is not set.", "source");
            }

            ICoordinateTransformation ct = GetTransformation(source.SRID, targetSrid);

            return Transform(source, ct);
        }

        public static IGeometry Transform(IGeometry source, ICoordinateTransformation ct)
        {
            if (source == null)
                return null;

            if (ct == null)
                throw new ArgumentException();

            Type t = source.GetType();
            if (t.Equals(typeof (Point)))
            {
                return Transform((Point) source, ct);
            }
            if (t.Equals(typeof (LineString)))
            {
                return Transform((LineString) source, ct);
            }
            if (t.Equals(typeof (Polygon)))
            {
                return Transform((Polygon) source, ct);
            }
            if (t.Equals(typeof (MultiPolygon)))
            {
                var mpoly = (MultiPolygon) source;
                var polys = (from Polygon poly in mpoly.Geometries select Transform(poly, ct) as Polygon).ToArray();

                return new MultiPolygon(polys);
            }
            if (t.Equals(typeof (MultiPoint)))
            {
                var mpoint = (MultiPoint) source;
                var points = (from Point point in mpoint.Geometries select Transform(point, ct) as Point).ToArray();

                return new MultiPoint(points);
            }
            if (t.Equals(typeof (MultiLineString)))
            {
                var mline = (MultiLineString) source;
                var lines = (from LineString line in mline.Geometries select Transform(line, ct) as LineString).ToArray();

                return new MultiLineString(lines);
            }

            throw new ArgumentException(string.Format("Type of {0} cannot transform.", source.GetType()));
        }

        /// <summary>
        /// Return a CoordinateTransformation object from list by SRIDs.
        /// If the object is not created, this makes a new one.
        /// </summary>
        /// <param name="sourceSrid"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        public static ICoordinateTransformation GetTransformation(int sourceSrid, int targetSrid)
        {
            if (TransList.Count > 0)
            {
                IEnumerable<ICoordinateTransformation> trans =
                    TransList.Where(
                        ct => ct.SourceCS.AuthorityCode == sourceSrid && ct.TargetCS.AuthorityCode == targetSrid);

                if (trans != null && trans.Count() > 0)
                    return trans.First();
            }

            ICoordinateTransformation newCt = CreateTranformation(sourceSrid, targetSrid);
            if (newCt != null)
                TransList.Add(newCt);
            return newCt;
        }

        public static IPolygon TransformExtentPolygon(IPolygon extent, int targetSrid)
        {
            IPolygon interoparatedExtent = extent;

            if (IsSimpleExtent(extent))
            {
                var coords = new List<ICoordinate>();
                ILineString l = extent.ExteriorRing;
                CreateHandledPoints(ref coords, l.GetPointN(0), l.GetPointN(1));
                CreateHandledPoints(ref coords, l.GetPointN(1), l.GetPointN(2));
                CreateHandledPoints(ref coords, l.GetPointN(2), l.GetPointN(3));
                CreateHandledPoints(ref coords, l.GetPointN(3), l.GetPointN(0));

                ILinearRing shell = geomUtil.CreateLinearRing(coords.ToArray());
                interoparatedExtent = geomUtil.CreatePolygon(shell, null);
                interoparatedExtent.SRID = extent.SRID;

                interoparatedExtent.ConvexHull();
            }

            return Transform(interoparatedExtent, targetSrid);
        }

        private static void CreateHandledPoints(ref List<ICoordinate> coords, IPoint startPoint, IPoint endPoint)
        {
            const int intervalCount = 100;
            double intervalX = (endPoint.X - startPoint.X) / intervalCount;
            double intervalY = (endPoint.Y - startPoint.Y) / intervalCount;

            coords.Add(startPoint.Coordinate);
            for (int i = 0; i < intervalCount - 1; i++)
            {
                ICoordinate p = new Coordinate(startPoint.X + intervalX * i, startPoint.Y + intervalY * i);
                coords.Add(p);
            }
            coords.Add(endPoint.Coordinate);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="extent"></param>
        /// <returns></returns>
        private static bool IsSimpleExtent(IPolygon extent)
        {
            if (extent.NumPoints != 5)
            {
                return false;
            }
            return true;
        }

        /// <summary>
        /// Create a new CoordinateTransformation object and cache it.
        /// </summary>
        /// <param name="sourceSrid"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        private static ICoordinateTransformation CreateTranformation(int sourceSrid, int targetSrid)
        {
            if (HasTransformation(sourceSrid, targetSrid))
            {
                return null;
            }

            ICoordinateSystem sourceCoordSystem = SridParser.GetCoordinateSystemFromSrid(sourceSrid);
            ICoordinateSystem targetCoordSystem = SridParser.GetCoordinateSystemFromSrid(targetSrid);
            var coordFactory = new CoordinateTransformationFactory();

            return coordFactory.CreateFromCoordinateSystems(sourceCoordSystem, targetCoordSystem);
        }

        /// <summary>
        /// Check if a CoordinateTransformation object available in the cache.
        /// </summary>
        /// <param name="sourceSrid"></param>
        /// <param name="targetSrid"></param>
        /// <returns></returns>
        private static bool HasTransformation(int sourceSrid, int targetSrid)
        {
            IEnumerable<ICoordinateTransformation> trans =
                TransList.Where(
                    ct => ct.SourceCS.AuthorityCode == sourceSrid && ct.TargetCS.AuthorityCode == targetSrid);

            if (trans.Count() != 0)
                return true;

            return false;
        }
    }
}